package javango.contrib.admin.views;

import static javango.contrib.freemarker.Helper.renderToResponse;
import static javango.util.Humanize.humanize;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javango.api.Settings;
import javango.contrib.admin.FieldSet;
import javango.contrib.admin.MetaOptions;
import javango.contrib.admin.api.AdminAction;
import javango.contrib.admin.api.AdminOptions;
import javango.contrib.admin.api.ModelAdmin;
import javango.contrib.hibernate.HibernateUtil;
import javango.contrib.hibernate.ModelForm;
import javango.contrib.jquery.Helper;
import javango.db.Manager;
import javango.db.ManagerException;
import javango.db.Managers;
import javango.db.PaginatorWidget;
import javango.db.QuerySet;
import javango.db.QuerySetPage;
import javango.db.QuerySetPaginator;
import javango.forms.Form;
import javango.forms.fields.CharField;
import javango.forms.fields.DateField;
import javango.forms.fields.Field;
import javango.forms.fields.FieldFactory;
import javango.forms.fields.annotations.FieldProperties;
import javango.http.Http404;
import javango.http.Http500;
import javango.http.HttpException;
import javango.http.HttpRequest;
import javango.http.HttpResponse;
import javango.http.HttpResponseRedirect;
import javango.http.SimpleHttpResponse;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.cfg.Configuration;
import org.hibernate.tool.hbm2ddl.SchemaExport;

import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Provider;

public class Main {

	private final static Log log = LogFactory.getLog(Main.class);
	
	protected Injector injector;
	protected HibernateUtil hibernateUtil;
	protected FieldFactory fieldFactory;
	protected Provider<Helper> jqueryProvider;
	protected AdminOptions options;
	protected Managers managers;
	protected Settings settings;
	
	@Inject
	public Main(Injector injector, Settings settings, HibernateUtil hibernateUtil, FieldFactory fieldFactory, Provider<Helper> jqueryProvider, AdminOptions options, Managers managers) {
		super();
		this.hibernateUtil = hibernateUtil;
		this.fieldFactory = fieldFactory; 
		this.jqueryProvider = jqueryProvider;
		this.options = options;
		this.managers = managers;
		this.settings = settings;
		this.injector = injector;
	}
	
	private String findVerboseName(Class clazz, ModelAdmin ma) {
		if (ma.getVerboseName() != null) {
			return ma.getVerboseName();
		}
		
		String className = clazz.getName();
		MetaOptions options = (MetaOptions)clazz.getAnnotation(MetaOptions.class);
		return options == null || "".equals(options.verboseName()) 
						? className.substring(className.lastIndexOf(".")+1) 
						: options.verboseName();
	}
	private Manager findManager(Class clazz, ModelAdmin ma) {
		Manager m = ma.getManager();
		if (m == null) {
			m = managers.forClass(clazz);
		}
		return m;
	}
	
	protected void updateContext(Map<String, Object> context , Class pc, ModelAdmin ma) {
		context.put("entity", new ModelDefinition(pc, ma));
		context.put("app_name", options.getAppName());
	}
	
	public class ModelDefinition {
		protected String className;
		protected String verboseName;
		
		public String getClassName() {
			return className;
		}

		public void setClassName(String className) {
			this.className = className;
		}

		public String getVerboseName() {
			return verboseName;
		}

		public void setVerboseName(String verboseName) {
			this.verboseName = verboseName;
		}

		public ModelDefinition(Class clazz, ModelAdmin ma) {
			super();
			this.className = clazz.getName();
			this.verboseName = findVerboseName(clazz, ma);
		}
		
		@Override
		public String toString() {
			return verboseName;
		}
		
	}
	
	public HttpResponse index(HttpRequest request) {
		Map<String, List<ModelDefinition>> packageMap = new HashMap<String, List<ModelDefinition>>();		
		
		Iterator<Class> i = options.getClassMappings();
		while (i.hasNext()) {
			Class pc = i.next();
			ModelAdmin ma = options.getModelAdmin(pc);
			if (ma.isAuthorized(request.getUser())) {
				String className = pc.getName();
				String packageName =  className.substring(0,className.lastIndexOf("."));
				if (log.isDebugEnabled()) log.debug ("Adding class: " + className);
				if (packageMap.containsKey(packageName)) {
					List<ModelDefinition> appList = packageMap.get(packageName);
					appList.add(new ModelDefinition(pc, ma));
				} else {
					List<ModelDefinition> appList = new ArrayList<ModelDefinition>();
					appList.add(new ModelDefinition(pc, ma));
					packageMap.put(packageName, appList);
				}
			} else if (log.isDebugEnabled()){
				log.debug("Skipping " + pc.getName() + " due to authority.");
			}
		}
		log.debug("Complete parsing class mappings");
		
		Map<String, Object> context = new HashMap<String, Object>();
		context.put("package_map", packageMap);
		
		context.put("page_title", "Site administration");
		context.put("app_name", options.getAppName());
		return renderToResponse(options.getIndexTemplate(), context);
	}
	
	protected int getPageNumber(HttpRequest request) {
		String value = request.getParameter("p");
		try {
			return value == null ? 1 : Integer.valueOf(value);
		} catch (NumberFormatException e) {
			log.debug(e,e);
			return 1;
		}
	}
	
	protected String generateSearchParams(HttpRequest request, Map<String, String> map) {
		StringBuilder b = new StringBuilder();
		for (Map.Entry<String, String[]> entry : request.getParameterMap().entrySet()) {			
			if (entry.getKey().contains("__")) {
				String[] value = entry.getValue();
				if (value != null && value.length > 0 & StringUtils.isNotEmpty(value[0])) {
					// TODO Add AND ability,  chain the String[] somehow...
					String valueString = entry.getValue()[0];
					if (map != null)
						try {
							map.put(entry.getKey(), valueString);
							b.append(String.format("%s=%s&", entry.getKey(), URLEncoder.encode(valueString, settings.getDefaultCharset())));
						} catch (UnsupportedEncodingException e) {
							log.error(e,e);
						}
				}
			}
		}
		return b.toString();
	}
	
	public HttpResponse search(HttpRequest request, String model) throws HttpException {
		Class pc = options.getClassMapping(model);
		if (pc == null) {
			throw new Http404("Class not found");
		}
		ModelAdmin ma = options.getModelAdmin(pc);
		
		if (!ma.isReader(request.getUser())) {
			throw new Http404("Class not found, or not authorized");
		}
		
		String[] property_list = ma.getListSearchFields();			

		Form form = ma.getSearchForm();

		if (form == null) {
			String[] includeFields = property_list == null ? getIncludeFields(pc) : property_list;

			ModelForm<?> mForm = new ModelForm(fieldFactory, hibernateUtil, managers);
			mForm.setInclude(includeFields);
			mForm.setModel(pc);

			for (Field f : mForm.getFields().values()) {
				f.setRequired(false).setAllowNull(true);
				f.setEditable(true);
				if (f.getClass().equals(CharField.class)) {
					f.setName(f.getName() + "__ilike");
				} else if (f.getClass().equals(DateField.class)) {
					f.setName(f.getName() + "__date");
				} else {
					f.setName(f.getName() + "__eq");
				}
			}
			
			form = mForm;
		}
		

		
		if ("POST".equals(request.getMethod())) {
			form.bind(request.getParameterMap());
			if (form.isValid()) {
				// TODO better way to get the relative 'admin' part of the url
				Map<String, String> params = new HashMap<String, String>();
				generateSearchParams(request, params);
				return new HttpResponseRedirect("../", params);
			}
		}
		
		Map<String, Object> context = new HashMap<String, Object>();
		context.put("form", form);
		context.put("jquery", jqueryProvider.get().setBasePath(request.getContext()).forForm(form));

		updateContext(context, pc, ma);
		
		context.put("page_title", String.format("%s Search", findVerboseName(pc, ma)));
		return renderToResponse(ma.getSearchFormTemplate(), context);

	}
	
	public HttpResponse changeList(HttpRequest request, String model) throws HttpException {
		try {
			Class pc = options.getClassMapping(model);
			if (pc == null) {
				throw new Http404("Class not found");
			}
			ModelAdmin ma = options.getModelAdmin(pc);
			
			if (!ma.isReader(request.getUser())) {
				throw new Http404("Class not found, or not authorized");
			}
			
			Manager<?> manager = findManager(pc, ma);

			String[] property_list = ma.getListDisplay();
			List<String> header_list = new ArrayList<String>();
			for(String property : property_list) {
				header_list.add(getChangeListLabel(pc, ma, property));
			}
			
			Map<String, String> searchParams = new HashMap<String, String>();
			for(Entry<String, String[]> e : request.getParameterMap().entrySet()) {
				if (e.getKey().contains("__")) {
					String value = e.getValue() == null ? null : e.getValue().length == 0 ? null : e.getValue()[0];
					searchParams.put(e.getKey(), value);
				}
			}
			
			QuerySet<?> qs = manager.filter(searchParams);
			String[] orderBy = ma.getOrderBy();
			if (orderBy == null) {
				qs = qs.orderBy(manager.getPkProperty());
			} else {
				qs = qs.orderBy();
			}
			String tool = request.getParameter("tool");
			// an action has been selected or clicked
			if (tool != null) {
				for (AdminAction action : ma.getListTools()) {
					if (tool.equals(action.getClass().getName())) {
						return action.execute(ma, request, qs);
					}
				}
			}
			
			QuerySetPaginator<?> paginator = new QuerySetPaginator(qs, 30);

			String query_string = generateSearchParams(request, searchParams);

			QuerySetPage<?> page = paginator.getPage(getPageNumber(request));
			List<?> object_list = page.getObjectList();
			
			Map<String, Object> context = new HashMap<String, Object>();
			context.put("header_list", header_list);
			context.put("property_list", property_list);
			context.put("object_list", object_list);
			context.put("key_property", manager.getPkProperty());
			context.put("link_property", ma.getListDisplayLinks() == null ? property_list[0] : ma.getListDisplayLinks());
			context.put("paginator", new PaginatorWidget(paginator, getPageNumber(request), query_string));
			context.put("query_string", query_string);
			context.put("list_tools", ma.getListTools());
			updateContext(context, pc, ma);
					
			context.put("page_title", String.format("Select %s to change", findVerboseName(pc, ma)));			
			return renderToResponse(ma.getChangeListTemplate(), context);
		} catch (ManagerException e) {
			throw new HttpException(e);
		}
	}
	
	/**
	 * Given a class and property return the Label for the change list.
	 * @param pc
	 * @param ma
	 * @param property
	 * @return
	 */
	private String getChangeListLabel(Class pc, ModelAdmin ma, String property) {
		// todo maybe add ability to add labels to modeladmin and add that here.
		FieldProperties properties = (FieldProperties)pc.getAnnotation(FieldProperties.class);
		String label = null;
		if (properties != null) {
			label = properties.label();
		}
		if (label == null) {
			String pieces[] = property.split(":");
			if (pieces.length > 1) { // includes a :
				label = pieces[0];
			} else { // does not include the :
				pieces = property.split("\\.");
				property = pieces[pieces.length-1];
				label = humanize(property);
			}
		}
		return label;
		
	}

	public HttpResponse add(HttpRequest request, String model) throws HttpException {		
		return change(request, model, null);
	}
	
	protected String[] getIncludeFields(Class pc) {
		FieldSet fieldSet = (FieldSet)pc.getAnnotation(FieldSet.class);
		if (fieldSet != null) {
			return fieldSet.fields();
		}
		
		return null;

	}
	
	public HttpResponse change(HttpRequest request, String model, String object_id) throws HttpException {
		try {
			object_id = object_id == null ? null : URLDecoder.decode(object_id, settings.getDefaultCharset());
			
			// TODO Changet object_id to an Object and figure out the correct datatype from the dao. dont' think this is necessary
			Class pc = options.getClassMapping(model);
			if (pc == null) {
				throw new Http404("Class not found");
			}
			ModelAdmin ma = options.getModelAdmin(pc);
			
			if (object_id != null && !ma.isEditor(request.getUser())) {
				throw new Http404("Class not found, or not authorized");
			} else if (!ma.isAuthor(request.getUser())) {
				throw new Http404("Class not found, or not authorized");
			}
			
			Manager manager = findManager(pc, ma);
			
			Object object = null;

			if (object_id != null) {
				object = manager.get(object_id);
				if (object == null) {
					return new SimpleHttpResponse("Object not found");
				}
			}

			String[] property_list = ma.getFields();			

			Form form = ma.getForm();
			
			if (form == null) {
				String[] includeFields = property_list == null ? getIncludeFields(pc) : property_list;
	
				ModelForm mForm = new ModelForm(fieldFactory, hibernateUtil, managers);
				if (includeFields != null) mForm.setInclude(includeFields);
				mForm.setModel(pc);
				form = mForm;
			}
			
			if ("POST".equals(request.getMethod())) {
				// update the object
				if (object == null) object=injector.getInstance(pc);
				
				form.bind(request.getParameterMap());
				// form = modelFactory.form(pc.getMappedClass(), request.getParameterMap());
				if (form.isValid()) {
//					if (object_id == null) { // this is a new object
//						if (!bl.canCreate()) {
//							throw new HttpException("Unable to create");
//						}
//					} else {
//						if (!bl.canUpdate(object)) {
//							throw new HttpException("Unable to updte");
//						}
//					}
					
					form.clean(object);
					try {
						if (object_id == null) {
							manager.create(object);
						} else {
							manager.save(object);
						}
						
						if (request.getParameterMap().containsKey("_addanother")) {
							return new HttpResponseRedirect("../add");
						} else if (request.getParameterMap().containsKey("_continue")) {
							return new HttpResponseRedirect(String.format("../%s/", manager.getPk(object)));
						}
						return new HttpResponseRedirect("..");
					} catch (ManagerException e) {
						request.getSession().addError(e.getMessage());
					}
				}
			} else {
				// display the form
				if (object != null) form.setInitial(object);
			}
					
			Map<String, Object> context = new HashMap<String, Object>();
			context.put("property_list", property_list);
			context.put("object", object);
			context.put("form", form);
			context.put("object_id", object_id);
			context.put("jquery", jqueryProvider.get().setBasePath(request.getContext()).forForm(form));
			updateContext(context, pc, ma);
					
			context.put("page_title", String.format("Change %s", findVerboseName(pc, ma)));
			return renderToResponse(ma.getChangeFormTemplate(), context);
		} catch (Exception e) {
			log.error(e,e);
			throw new HttpException(e);
		}
	}
	
	public HttpResponse delete(HttpRequest request, String model, String object_id) throws HttpException {
		try {
			object_id = object_id == null ? null : URLDecoder.decode(object_id, settings.getDefaultCharset());

			Class pc = options.getClassMapping(model);
			if (pc == null) {
				throw new Http404("Class not found");
			}
			ModelAdmin ma = options.getModelAdmin(pc);
			
			Manager manager = findManager(pc, ma);

			Object object = manager.get(object_id);
			if (object == null) {
				throw new Http404("Not found"); // TODO 404
			}
	
//			if (!bl.canDelete(object)) {
//				throw new HttpException("Unauthorized to delete");
//			}
				
			if ("POST".equals(request.getMethod())) {
				try {
					manager.delete(object);
					return new HttpResponseRedirect("../..");
				} catch (ManagerException e) {
					request.getSession().addError(e.getMessage());
				}
			}
			
			Map<String, Object> context = new HashMap<String, Object>();
			context.put("object", object);
			updateContext(context, pc, ma);
			
			context.put("page_title", "Are you sure?");
			return renderToResponse(ma.getConfirmDeleteTemplate(), context);
		} catch (Exception e) {
			log.error(e,e);
			throw new HttpException(e);
		}
	}
	
	public HttpResponse resetdb(HttpRequest request) {
		if ("POST".equals(request.getMethod())) {
			Configuration cfg = hibernateUtil.getConfiguration();
			new SchemaExport(cfg).drop(false, true);
			new SchemaExport(cfg).create(false, true);
			request.getSession().addMessage("Tables created successfully");
			return new HttpResponseRedirect("..");
		}
		
		Map<String, Object> context = new HashMap<String, Object>();
		context.put("page_title", "Are you sure?");
		return renderToResponse("/javango/contrib/admin/templates/confirm_reset_db.ftl", context);
	}
}
